/** @file
  Internal floppy disk controller programming functions for the floppy driver.

Copyright (c) 2006 - 2018, Intel Corporation. All rights reserved.<BR>
SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include "IsaFloppy.h"

/**
  Detect whether a floppy drive is present or not.

  @param[in] FdcDev  A pointer to the FDC_BLK_IO_DEV

  @retval EFI_SUCCESS    The floppy disk drive is present
  @retval EFI_NOT_FOUND  The floppy disk drive is not present
**/
EFI_STATUS
DiscoverFddDevice (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;

  FdcDev->BlkIo.Media = &FdcDev->BlkMedia;

  Status = FddIdentify (FdcDev);
  if (EFI_ERROR (Status)) {
    return EFI_NOT_FOUND;
  }

  FdcDev->BlkIo.Reset               = FdcReset;
  FdcDev->BlkIo.FlushBlocks         = FddFlushBlocks;
  FdcDev->BlkIo.ReadBlocks          = FddReadBlocks;
  FdcDev->BlkIo.WriteBlocks         = FddWriteBlocks;
  FdcDev->BlkMedia.LogicalPartition = FALSE;
  FdcDev->BlkMedia.WriteCaching     = FALSE;

  return EFI_SUCCESS;
}

/**
  Do recalibrate and check if the drive is present or not
  and set the media parameters if the driver is present.

  @param[in] FdcDev  A pointer to the FDC_BLK_IO_DEV

  @retval EFI_SUCCESS       The floppy disk drive is present
  @retval EFI_DEVICE_ERROR  The floppy disk drive is not present
**/
EFI_STATUS
FddIdentify (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;

  //
  // Set Floppy Disk Controller's motor on
  //
  Status = MotorOn (FdcDev);
  if (EFI_ERROR (Status)) {
    return EFI_DEVICE_ERROR;
  }

  Status = Recalibrate (FdcDev);

  if (EFI_ERROR (Status)) {
    MotorOff (FdcDev);
    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }
  //
  // Set Media Parameter
  //
  FdcDev->BlkIo.Media->RemovableMedia = TRUE;
  FdcDev->BlkIo.Media->MediaPresent   = TRUE;
  FdcDev->BlkIo.Media->MediaId = 0;

  //
  // Check Media
  //
  Status = DisketChanged (FdcDev);

  if (Status == EFI_NO_MEDIA) {
    FdcDev->BlkIo.Media->MediaPresent = FALSE;
  } else if ((Status != EFI_MEDIA_CHANGED) &&
             (Status != EFI_SUCCESS)) {
    MotorOff (FdcDev);
    return Status;
  }

  //
  // Check Disk Write Protected
  //
  Status = SenseDrvStatus (FdcDev, 0);

  if (Status == EFI_WRITE_PROTECTED) {
    FdcDev->BlkIo.Media->ReadOnly = TRUE;
  } else if (Status == EFI_SUCCESS) {
    FdcDev->BlkIo.Media->ReadOnly = FALSE;
  } else {
    return EFI_DEVICE_ERROR;
  }

  MotorOff (FdcDev);

  //
  // Set Media Default Type
  //
  FdcDev->BlkIo.Media->BlockSize  = DISK_1440K_BYTEPERSECTOR;
  FdcDev->BlkIo.Media->LastBlock  = DISK_1440K_EOT * 2 * (DISK_1440K_MAXTRACKNUM + 1) - 1;

  return EFI_SUCCESS;
}

/**
  Reset the Floppy Logic Drive.

  @param  FdcDev FDC_BLK_IO_DEV * : A pointer to the FDC_BLK_IO_DEV

  @retval EFI_SUCCESS:    The Floppy Logic Drive is reset
  @retval EFI_DEVICE_ERROR: The Floppy Logic Drive is not functioning correctly and
                      can not be reset

**/
EFI_STATUS
FddReset (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  UINT8 Data;
  UINT8 StatusRegister0;
  UINT8 PresentCylinderNumber;
  UINTN Index;

  //
  // Report reset progress code
  //
  REPORT_STATUS_CODE_WITH_DEVICE_PATH (
    EFI_PROGRESS_CODE,
    EFI_PERIPHERAL_REMOVABLE_MEDIA | EFI_P_PC_RESET,
    FdcDev->DevicePath
    );

  //
  // Reset specified Floppy Logic Drive according to FdcDev -> Disk
  // Set Digital Output Register(DOR) to do reset work
  //   bit0 & bit1 of DOR : Drive Select
  //   bit2 : Reset bit
  //   bit3 : DMA and Int bit
  // Reset : a "0" written to bit2 resets the FDC, this reset will remain
  //         active until
  //         a "1" is written to this bit.
  // Reset step 1:
  //         use bit0 & bit1 to  select the logic drive
  //         write "0" to bit2
  //
  Data = 0x0;
  Data = (UINT8) (Data | (SELECT_DRV & FdcDev->Disk));
  FdcWritePort (FdcDev, FDC_REGISTER_DOR, Data);

  //
  // wait some time,at least 120us
  //
  MicroSecondDelay (500);

  //
  // Reset step 2:
  //   write "1" to bit2
  //   write "1" to bit3 : enable DMA
  //
  Data |= 0x0C;
  FdcWritePort (FdcDev, FDC_REGISTER_DOR, Data);

  //
  // Experience value
  //
  MicroSecondDelay (2000);

  //
  // wait specified floppy logic drive is not busy
  //
  if (EFI_ERROR (FddWaitForBSYClear (FdcDev, 1))) {
    return EFI_DEVICE_ERROR;
  }
  //
  // Set the Transfer Data Rate
  //
  FdcWritePort (FdcDev, FDC_REGISTER_CCR, 0x0);

  //
  // Experience value
  //
  MicroSecondDelay (100);

  //
  // Issue Sense interrupt command for each drive (total 4 drives)
  //
  for (Index = 0; Index < 4; Index++) {
    if (EFI_ERROR (SenseIntStatus (FdcDev, &StatusRegister0, &PresentCylinderNumber))) {
      return EFI_DEVICE_ERROR;
    }
  }
  //
  // issue Specify command
  //
  if (EFI_ERROR (Specify (FdcDev))) {
    return EFI_DEVICE_ERROR;
  }

  return EFI_SUCCESS;
}

/**
  Turn the floppy disk drive's motor on.
  The drive's motor must be on before any command can be executed.

  @param[in] FdcDev  A pointer to the FDC_BLK_IO_DEV

  @retval  EFI_SUCCESS            The drive's motor was turned on successfully
  @retval  EFI_DEVICE_ERROR       The drive is busy, so can not turn motor on
**/
EFI_STATUS
MotorOn (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;
  UINT8       DorData;

  //
  // Control of the floppy drive motors is a big pain. If motor is off, you have
  // to turn it on first. But you can not leave the motor on all the time, since
  // that would wear out the disk. On the other hand, if you turn the motor off
  // after each operation, the system performance will be awful. The compromise
  // used in this driver is to leave the motor on for 2 seconds after
  // each operation. If a new operation is started in that interval(2s),
  // the motor need not be turned on again. If no new operation is started,
  // a timer goes off and the motor is turned off
  //
  //
  // Cancel the timer
  //
  Status = gBS->SetTimer (FdcDev->Event, TimerCancel, 0);
  ASSERT_EFI_ERROR (Status);

  //
  // Get the motor status
  //
  DorData = FdcReadPort (FdcDev, FDC_REGISTER_DOR);

  if (((FdcDev->Disk == FdcDisk0) && ((DorData & 0x10) == 0x10)) ||
      ((FdcDev->Disk == FdcDisk1) && ((DorData & 0x21) == 0x21))
      ) {
    return EFI_SUCCESS;
  }
  //
  // The drive's motor is off, so need turn it on
  // first look at command and drive are busy or not
  //
  if (EFI_ERROR (FddWaitForBSYClear (FdcDev, 1))) {
    return EFI_DEVICE_ERROR;
  }
  //
  // for drive A: 1CH, drive B: 2DH
  //
  DorData = 0x0C;
  DorData = (UINT8) (DorData | (SELECT_DRV & FdcDev->Disk));
  if (FdcDev->Disk == FdcDisk0) {
    //
    // drive A
    //
    DorData |= DRVA_MOTOR_ON;
  } else {
    //
    // drive B
    //
    DorData |= DRVB_MOTOR_ON;
  }

  FdcWritePort (FdcDev, FDC_REGISTER_DOR, DorData);

  //
  // Experience value
  //
  MicroSecondDelay (4000);

  return EFI_SUCCESS;
}

/**
  Set a Timer and when Timer goes off, turn the motor off.

  @param[in] FdcDev  A pointer to the FDC_BLK_IO_DEV

  @retval  EFI_SUCCESS            Set the Timer successfully
  @retval  EFI_INVALID_PARAMETER  Fail to Set the timer
**/
EFI_STATUS
MotorOff (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  //
  // Set the timer : 2s
  //
  return gBS->SetTimer (FdcDev->Event, TimerRelative, 20000000);
}

/**
  Detect whether the disk in the drive is changed or not.

  @param[in] FdcDev  A pointer to FDC_BLK_IO_DEV

  @retval  EFI_SUCCESS        No disk media change
  @retval  EFI_DEVICE_ERROR   Fail to do the recalibrate or seek operation
  @retval  EFI_NO_MEDIA       No disk in the drive
  @retval  EFI_MEDIA_CHANGED  There is a new disk in the drive
**/
EFI_STATUS
DisketChanged (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;
  UINT8       Data;

  //
  // Check change line
  //
  Data = FdcReadPort (FdcDev, FDC_REGISTER_DIR);

  //
  // Io delay
  //
  MicroSecondDelay (50);

  if ((Data & DIR_DCL) == 0x80) {
    //
    // disk change line is active
    //
    if (FdcDev->PresentCylinderNumber != 0) {
      Status = Recalibrate (FdcDev);
    } else {
      Status = Seek (FdcDev, 0x30);
    }

    if (EFI_ERROR (Status)) {
      FdcDev->ControllerState->NeedRecalibrate = TRUE;
      return EFI_DEVICE_ERROR;
      //
      // Fail to do the seek or recalibrate operation
      //
    }

    Data = FdcReadPort (FdcDev, FDC_REGISTER_DIR);

    //
    // Io delay
    //
    MicroSecondDelay (50);

    if ((Data & DIR_DCL) == 0x80) {
      return EFI_NO_MEDIA;
    }

    return EFI_MEDIA_CHANGED;
  }

  return EFI_SUCCESS;
}

/**
  Do the Specify command, this command sets DMA operation
  and the initial values for each of the three internal
  times: HUT, SRT and HLT.

  @param[in] FdcDev  Pointer to instance of FDC_BLK_IO_DEV

  @retval EFI_SUCCESS       Execute the Specify command successfully
  @retval EFI_DEVICE_ERROR  Fail to execute the command
**/
EFI_STATUS
Specify (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  FDD_SPECIFY_CMD Command;
  UINTN           Index;
  UINT8           *CommandPointer;

  ZeroMem (&Command, sizeof (FDD_SPECIFY_CMD));
  Command.CommandCode = SPECIFY_CMD;
  //
  // set SRT, HUT
  //
  Command.SrtHut = 0xdf;
  //
  // 0xdf;
  //
  // set HLT and DMA
  //
  Command.HltNd   = 0x02;

  CommandPointer  = (UINT8 *) (&Command);
  for (Index = 0; Index < sizeof (FDD_SPECIFY_CMD); Index++) {
    if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }

  return EFI_SUCCESS;
}

/**
  Set the head of floppy drive to track 0.

  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to FDC_BLK_IO_DEV
  @retval EFI_SUCCESS:    Execute the Recalibrate operation successfully
  @retval EFI_DEVICE_ERROR: Fail to execute the Recalibrate operation

**/
EFI_STATUS
Recalibrate (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  FDD_COMMAND_PACKET2 Command;
  UINTN               Index;
  UINT8               StatusRegister0;
  UINT8               PresentCylinderNumber;
  UINT8               *CommandPointer;
  UINT8               Count;

  Count = 2;

  while (Count > 0) {
    ZeroMem (&Command, sizeof (FDD_COMMAND_PACKET2));
    Command.CommandCode = RECALIBRATE_CMD;
    //
    // drive select
    //
    if (FdcDev->Disk == FdcDisk0) {
      Command.DiskHeadSel = 0;
      //
      // 0
      //
    } else {
      Command.DiskHeadSel = 1;
      //
      // 1
      //
    }

    CommandPointer = (UINT8 *) (&Command);
    for (Index = 0; Index < sizeof (FDD_COMMAND_PACKET2); Index++) {
      if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
        return EFI_DEVICE_ERROR;
      }
    }
    //
    // Experience value
    //
    MicroSecondDelay (250000);
    //
    // need modify according to 1.44M or 2.88M
    //
    if (EFI_ERROR (SenseIntStatus (FdcDev, &StatusRegister0, &PresentCylinderNumber))) {
      return EFI_DEVICE_ERROR;
    }

    if ((StatusRegister0 & 0xf0) == 0x20 && PresentCylinderNumber == 0) {
      FdcDev->PresentCylinderNumber             = 0;
      FdcDev->ControllerState->NeedRecalibrate  = FALSE;
      return EFI_SUCCESS;
    } else {
      Count--;
      if (Count == 0) {
        return EFI_DEVICE_ERROR;
      }
    }
  }
  //
  // end while
  //
  return EFI_SUCCESS;
}

/**
  Set the head of floppy drive to the new cylinder.

  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to FDC_BLK_IO_DEV
  @param  Lba EFI_LBA     : The logic block address want to seek

  @retval  EFI_SUCCESS:    Execute the Seek operation successfully
  @retval  EFI_DEVICE_ERROR: Fail to execute the Seek operation

**/
EFI_STATUS
Seek (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN EFI_LBA         Lba
  )
{
  FDD_SEEK_CMD  Command;
  UINT8         EndOfTrack;
  UINT8         Head;
  UINT8         Cylinder;
  UINT8         StatusRegister0;
  UINT8         *CommandPointer;
  UINT8         PresentCylinderNumber;
  UINTN         Index;
  UINT8         DelayTime;

  if (FdcDev->ControllerState->NeedRecalibrate) {
    if (EFI_ERROR (Recalibrate (FdcDev))) {
      FdcDev->ControllerState->NeedRecalibrate = TRUE;
      return EFI_DEVICE_ERROR;
    }
  }

  EndOfTrack = DISK_1440K_EOT;
  //
  // Calculate cylinder based on Lba and EOT
  //
  Cylinder = (UINT8) ((UINTN) Lba / EndOfTrack / 2);

  //
  // if the destination cylinder is the present cylinder, unnecessary to do the
  // seek operation
  //
  if (FdcDev->PresentCylinderNumber == Cylinder) {
    return EFI_SUCCESS;
  }
  //
  // Calculate the head : 0 or 1
  //
  Head = (UINT8) ((UINTN) Lba / EndOfTrack % 2);

  ZeroMem (&Command, sizeof (FDD_SEEK_CMD));
  Command.CommandCode = SEEK_CMD;
  if (FdcDev->Disk == FdcDisk0) {
    Command.DiskHeadSel = 0;
    //
    // 0
    //
  } else {
    Command.DiskHeadSel = 1;
    //
    // 1
    //
  }

  Command.DiskHeadSel = (UINT8) (Command.DiskHeadSel | (Head << 2));
  Command.NewCylinder = Cylinder;

  CommandPointer      = (UINT8 *) (&Command);
  for (Index = 0; Index < sizeof (FDD_SEEK_CMD); Index++) {
    if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }
  //
  // Io delay
  //
  MicroSecondDelay (100);

  //
  // Calculate waiting time
  //
  if (FdcDev->PresentCylinderNumber > Cylinder) {
    DelayTime = (UINT8) (FdcDev->PresentCylinderNumber - Cylinder);
  } else {
    DelayTime = (UINT8) (Cylinder - FdcDev->PresentCylinderNumber);
  }

  MicroSecondDelay ((DelayTime + 1) * 4000);

  if (EFI_ERROR (SenseIntStatus (FdcDev, &StatusRegister0, &PresentCylinderNumber))) {
    return EFI_DEVICE_ERROR;
  }

  if ((StatusRegister0 & 0xf0) == 0x20) {
    FdcDev->PresentCylinderNumber = Command.NewCylinder;
    return EFI_SUCCESS;
  } else {
    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }
}

/**
  Do the Sense Interrupt Status command, this command
  resets the interrupt signal.

  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to FDC_BLK_IO_DEV
  @param  StatusRegister0 UINT8 *: Be used to save Status Register 0 read from FDC
  @param  PresentCylinderNumber  UINT8 *: Be used to save present cylinder number
                                    read from FDC

  @retval  EFI_SUCCESS:    Execute the Sense Interrupt Status command successfully
  @retval  EFI_DEVICE_ERROR: Fail to execute the command

**/
EFI_STATUS
SenseIntStatus (
  IN     FDC_BLK_IO_DEV  *FdcDev,
  IN OUT UINT8           *StatusRegister0,
  IN OUT UINT8           *PresentCylinderNumber
  )
{
  UINT8 Command;

  Command = SENSE_INT_STATUS_CMD;
  if (EFI_ERROR (DataOutByte (FdcDev, &Command))) {
    return EFI_DEVICE_ERROR;
  }

  if (EFI_ERROR (DataInByte (FdcDev, StatusRegister0))) {
    return EFI_DEVICE_ERROR;
  }

  if (EFI_ERROR (DataInByte (FdcDev, PresentCylinderNumber))) {
    return EFI_DEVICE_ERROR;
  }

  return EFI_SUCCESS;
}

/**
  Do the Sense Drive Status command.

  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to FDC_BLK_IO_DEV
  @param  Lba EFI_LBA     : Logic block address

  @retval  EFI_SUCCESS:    Execute the Sense Drive Status command successfully
  @retval  EFI_DEVICE_ERROR: Fail to execute the command
  @retval  EFI_WRITE_PROTECTED:The disk is write protected

**/
EFI_STATUS
SenseDrvStatus (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN EFI_LBA         Lba
  )
{
  FDD_COMMAND_PACKET2 Command;
  UINT8               Head;
  UINT8               EndOfTrack;
  UINTN               Index;
  UINT8               StatusRegister3;
  UINT8               *CommandPointer;

  //
  // Sense Drive Status command obtains drive status information,
  // it has not execution phase and goes directly to the result phase from the
  // command phase, Status Register 3 contains the drive status information
  //
  ZeroMem (&Command, sizeof (FDD_COMMAND_PACKET2));
  Command.CommandCode = SENSE_DRV_STATUS_CMD;

  if (FdcDev->Disk == FdcDisk0) {
    Command.DiskHeadSel = 0;
  } else {
    Command.DiskHeadSel = 1;
  }

  EndOfTrack  = DISK_1440K_EOT;
  Head        = (UINT8) ((UINTN) Lba / EndOfTrack % 2);
  Command.DiskHeadSel = (UINT8) (Command.DiskHeadSel | (Head << 2));

  CommandPointer = (UINT8 *) (&Command);
  for (Index = 0; Index < sizeof (FDD_COMMAND_PACKET2); Index++) {
    if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }

  if (EFI_ERROR (DataInByte (FdcDev, &StatusRegister3))) {
    return EFI_DEVICE_ERROR;
  }
  //
  // Io delay
  //
  MicroSecondDelay (50);

  //
  // Check Status Register 3 to get drive status information
  //
  return CheckStatus3 (StatusRegister3);
}

/**
  Update the disk media properties and if necessary reinstall Block I/O interface.

  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to FDC_BLK_IO_DEV

  @retval  EFI_SUCCESS:    Do the operation successfully
  @retval  EFI_DEVICE_ERROR: Fail to the operation

**/
EFI_STATUS
DetectMedia (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;
  BOOLEAN     Reset;
  BOOLEAN     ReadOnlyLastTime;
  BOOLEAN     MediaPresentLastTime;

  Reset                = FALSE;
  ReadOnlyLastTime     = FdcDev->BlkIo.Media->ReadOnly;
  MediaPresentLastTime = FdcDev->BlkIo.Media->MediaPresent;

  //
  // Check disk change
  //
  Status = DisketChanged (FdcDev);

  if (Status == EFI_MEDIA_CHANGED) {
    FdcDev->BlkIo.Media->MediaId++;
    FdcDev->BlkIo.Media->MediaPresent = TRUE;
    Reset = TRUE;
  } else if (Status == EFI_NO_MEDIA) {
    FdcDev->BlkIo.Media->MediaPresent = FALSE;
  } else if (Status != EFI_SUCCESS) {
    MotorOff (FdcDev);
    return Status;
    //
    // EFI_DEVICE_ERROR
    //
  }

  if (FdcDev->BlkIo.Media->MediaPresent) {
    //
    // Check disk write protected
    //
    Status = SenseDrvStatus (FdcDev, 0);
    if (Status == EFI_WRITE_PROTECTED) {
      FdcDev->BlkIo.Media->ReadOnly = TRUE;
    } else {
      FdcDev->BlkIo.Media->ReadOnly = FALSE;
    }
  }

  if (FdcDev->BlkIo.Media->MediaPresent && (ReadOnlyLastTime != FdcDev->BlkIo.Media->ReadOnly)) {
    Reset = TRUE;
  }

  if (MediaPresentLastTime != FdcDev->BlkIo.Media->MediaPresent) {
    Reset = TRUE;
  }

  if (Reset) {
    Status = gBS->ReinstallProtocolInterface (
                    FdcDev->Handle,
                    &gEfiBlockIoProtocolGuid,
                    &FdcDev->BlkIo,
                    &FdcDev->BlkIo
                    );

    if (EFI_ERROR (Status)) {
      return Status;
    }
  }

  return EFI_SUCCESS;
}

/**
  Set the data rate and so on.

  @param  FdcDev  A pointer to FDC_BLK_IO_DEV

  @retval EFI_SUCCESS success to set the data rate
**/
EFI_STATUS
Setup (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;

  //
  // Set data rate 500kbs
  //
  FdcWritePort (FdcDev, FDC_REGISTER_CCR, 0x0);

  //
  // Io delay
  //
  MicroSecondDelay (50);

  Status = Specify (FdcDev);

  if (EFI_ERROR (Status)) {
    return EFI_DEVICE_ERROR;
  }

  return EFI_SUCCESS;
}

/**
  Read or Write a number of blocks in the same cylinder.

  @param  FdcDev      A pointer to FDC_BLK_IO_DEV
  @param  HostAddress device address
  @param  Lba         The starting logic block address to read from on the device
  @param  NumberOfBlocks The number of block wanted to be read or write
  @param  Read        Operation type: read or write

  @retval EFI_SUCCESS Success operate

**/
EFI_STATUS
ReadWriteDataSector (
  IN  FDC_BLK_IO_DEV  *FdcDev,
  IN  VOID            *HostAddress,
  IN  EFI_LBA         Lba,
  IN  UINTN           NumberOfBlocks,
  IN  BOOLEAN         Read
  )
{
  EFI_STATUS                                    Status;
  FDD_COMMAND_PACKET1                           Command;
  FDD_RESULT_PACKET                             Result;
  UINTN                                         Index;
  UINTN                                         Times;
  UINT8                                         *CommandPointer;

  EFI_PHYSICAL_ADDRESS                          DeviceAddress;
  EFI_ISA_IO_PROTOCOL                           *IsaIo;
  UINTN                                         NumberofBytes;
  VOID                                          *Mapping;
  EFI_ISA_IO_PROTOCOL_OPERATION                 Operation;
  EFI_STATUS                                    Status1;
  UINT8                                         Channel;
  EFI_ISA_ACPI_RESOURCE                         *ResourceItem;
  UINT32                                        Attribute;

  Status = Seek (FdcDev, Lba);
  if (EFI_ERROR (Status)) {
    return EFI_DEVICE_ERROR;
  }
  //
  // Map Dma
  //
  IsaIo         = FdcDev->IsaIo;
  NumberofBytes = NumberOfBlocks * 512;
  if (Read == READ) {
    Operation = EfiIsaIoOperationSlaveWrite;
  } else {
    Operation = EfiIsaIoOperationSlaveRead;
  }

  ResourceItem  = IsaIo->ResourceList->ResourceItem;
  Index         = 0;
  while (ResourceItem[Index].Type != EfiIsaAcpiResourceEndOfList) {
    if (ResourceItem[Index].Type == EfiIsaAcpiResourceDma) {
      break;
    }

    Index++;
  }

  if (ResourceItem[Index].Type == EfiIsaAcpiResourceEndOfList) {
    return EFI_DEVICE_ERROR;
  }

  Channel   = (UINT8) IsaIo->ResourceList->ResourceItem[Index].StartRange;
  Attribute = IsaIo->ResourceList->ResourceItem[Index].Attribute;

  Status1 = IsaIo->Map (
                    IsaIo,
                    Operation,
                    Channel,
                    Attribute,
                    HostAddress,
                    &NumberofBytes,
                    &DeviceAddress,
                    &Mapping
                    );
  if (EFI_ERROR (Status1)) {
    return Status1;
  }

  //
  // Allocate Read or Write command packet
  //
  ZeroMem (&Command, sizeof (FDD_COMMAND_PACKET1));
  if (Read == READ) {
    Command.CommandCode = READ_DATA_CMD | CMD_MT | CMD_MFM | CMD_SK;
  } else {
    Command.CommandCode = WRITE_DATA_CMD | CMD_MT | CMD_MFM;
  }

  FillPara (FdcDev, Lba, &Command);

  //
  // Write command bytes to FDC
  //
  CommandPointer = (UINT8 *) (&Command);
  for (Index = 0; Index < sizeof (FDD_COMMAND_PACKET1); Index++) {
    if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }
  //
  // wait for some time
  //
  Times = (STALL_1_SECOND / 50) + 1;
  do {
    if ((FdcReadPort (FdcDev, FDC_REGISTER_MSR) & 0xc0) == 0xc0) {
      break;
    }

    MicroSecondDelay (50);
    Times = Times - 1;
  } while (Times > 0);

  if (Times == 0) {
    return EFI_TIMEOUT;
  }
  //
  // Read result bytes from FDC
  //
  CommandPointer = (UINT8 *) (&Result);
  for (Index = 0; Index < sizeof (FDD_RESULT_PACKET); Index++) {
    if (EFI_ERROR (DataInByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }
  //
  // Flush before Unmap
  //
  if (Read == READ) {
    Status1 = IsaIo->Flush (IsaIo);
    if (EFI_ERROR (Status1)) {
      return Status1;
    }
  }
  //
  // Unmap Dma
  //
  Status1 = IsaIo->Unmap (IsaIo, Mapping);
  if (EFI_ERROR (Status1)) {
    return Status1;
  }

  return CheckResult (&Result, FdcDev);
}

/**
  Fill in FDD command's parameter.

  @param FdcDev   Pointer to instance of FDC_BLK_IO_DEV
  @param Lba      The starting logic block address to read from on the device
  @param Command  FDD command

**/
VOID
FillPara (
  IN  FDC_BLK_IO_DEV       *FdcDev,
  IN  EFI_LBA              Lba,
  IN  FDD_COMMAND_PACKET1  *Command
  )
{
  UINT8 EndOfTrack;

  //
  // Get EndOfTrack from the Para table
  //
  EndOfTrack = DISK_1440K_EOT;

  //
  // Fill the command parameter
  //
  if (FdcDev->Disk == FdcDisk0) {
    Command->DiskHeadSel = 0;
  } else {
    Command->DiskHeadSel = 1;
  }

  Command->Cylinder = (UINT8) ((UINTN) Lba / EndOfTrack / 2);
  Command->Head     = (UINT8) ((UINTN) Lba / EndOfTrack % 2);
  Command->Sector   = (UINT8) ((UINT8) ((UINTN) Lba % EndOfTrack) + 1);
  Command->DiskHeadSel = (UINT8) (Command->DiskHeadSel | (Command->Head << 2));
  Command->Number     = DISK_1440K_NUMBER;
  Command->EndOfTrack = DISK_1440K_EOT;
  Command->GapLength  = DISK_1440K_GPL;
  Command->DataLength = DISK_1440K_DTL;
}

/**
  Read result byte from Data Register of FDC.

  @param FdcDev   Pointer to instance of FDC_BLK_IO_DEV
  @param Pointer  Buffer to store the byte read from FDC

  @retval EFI_SUCCESS       Read result byte from FDC successfully
  @retval EFI_DEVICE_ERROR  The FDC is not ready to be read

**/
EFI_STATUS
DataInByte (
  IN  FDC_BLK_IO_DEV  *FdcDev,
  OUT UINT8           *Pointer
  )
{
  UINT8 Data;

  //
  // wait for 1ms and detect the FDC is ready to be read
  //
  if (EFI_ERROR (FddDRQReady (FdcDev, DATA_IN, 1))) {
    return EFI_DEVICE_ERROR;
    //
    // is not ready
    //
  }

  Data = FdcReadPort (FdcDev, FDC_REGISTER_DTR);

  //
  // Io delay
  //
  MicroSecondDelay (50);

  *Pointer = Data;
  return EFI_SUCCESS;
}

/**
  Write command byte to Data Register of FDC.

  @param FdcDev  Pointer to instance of FDC_BLK_IO_DEV
  @param Pointer Be used to save command byte written to FDC

  @retval  EFI_SUCCESS:    Write command byte to FDC successfully
  @retval  EFI_DEVICE_ERROR: The FDC is not ready to be written

**/
EFI_STATUS
DataOutByte (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN UINT8           *Pointer
  )
{
  UINT8 Data;

  //
  // wait for 1ms and detect the FDC is ready to be written
  //
  if (EFI_ERROR (FddDRQReady (FdcDev, DATA_OUT, 1))) {
    //
    // Not ready
    //
    return EFI_DEVICE_ERROR;
  }

  Data = *Pointer;

  FdcWritePort (FdcDev, FDC_REGISTER_DTR, Data);

  //
  // Io delay
  //
  MicroSecondDelay (50);

  return EFI_SUCCESS;
}

/**
  Detect the specified floppy logic drive is busy or not within a period of time.

  @param FdcDev           Indicate it is drive A or drive B
  @param Timeout          The time period for waiting

  @retval EFI_SUCCESS:  The drive and command are not busy
  @retval EFI_TIMEOUT:  The drive or command is still busy after a period time that
                        set by Timeout

**/
EFI_STATUS
FddWaitForBSYClear (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN UINTN           Timeout
  )
{
  UINTN Delay;
  UINT8 StatusRegister;
  UINT8 Mask;

  //
  // How to determine drive and command are busy or not: by the bits of
  // Main Status Register
  // bit0: Drive 0 busy (drive A)
  // bit1: Drive 1 busy (drive B)
  // bit4: Command busy
  //
  //
  // set mask: for drive A set bit0 & bit4; for drive B set bit1 & bit4
  //
  Mask  = (UINT8) ((FdcDev->Disk == FdcDisk0 ? MSR_DAB : MSR_DBB) | MSR_CB);

  Delay = ((Timeout * STALL_1_MSECOND) / 50) + 1;
  do {
    StatusRegister = FdcReadPort (FdcDev, FDC_REGISTER_MSR);
    if ((StatusRegister & Mask) == 0x00) {
      break;
      //
      // not busy
      //
    }

    MicroSecondDelay (50);
    Delay = Delay - 1;
  } while (Delay > 0);

  if (Delay == 0) {
    return EFI_TIMEOUT;
  }

  return EFI_SUCCESS;
}

/**
  Determine whether FDC is ready to write or read.

  @param  FdcDev Pointer to instance of FDC_BLK_IO_DEV
  @param  Dio BOOLEAN:      Indicate the FDC is waiting to write or read
  @param  Timeout           The time period for waiting

  @retval EFI_SUCCESS:  FDC is ready to write or read
  @retval EFI_NOT_READY:  FDC is not ready within the specified time period

**/
EFI_STATUS
FddDRQReady (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN BOOLEAN         Dio,
  IN UINTN           Timeout
  )
{
  UINTN Delay;
  UINT8 StatusRegister;
  UINT8 DataInOut;

  //
  // Before writing to FDC or reading from FDC, the Host must examine
  // the bit7(RQM) and bit6(DIO) of the Main Status Register.
  // That is to say:
  //  command bytes can not be written to Data Register
  //  unless RQM is 1 and DIO is 0
  //  result bytes can not be read from Data Register
  //  unless RQM is 1 and DIO is 1
  //
  DataInOut = (UINT8) (Dio << 6);
  //
  // in order to compare bit6
  //
  Delay = ((Timeout * STALL_1_MSECOND) / 50) + 1;
  do {
    StatusRegister = FdcReadPort (FdcDev, FDC_REGISTER_MSR);
    if ((StatusRegister & MSR_RQM) == MSR_RQM && (StatusRegister & MSR_DIO) == DataInOut) {
      break;
      //
      // FDC is ready
      //
    }

    MicroSecondDelay (50);
    //
    // Stall for 50 us
    //
    Delay = Delay - 1;
  } while (Delay > 0);

  if (Delay == 0) {
    return EFI_NOT_READY;
    //
    // FDC is not ready within the specified time period
    //
  }

  return EFI_SUCCESS;
}

/**
  Set FDC control structure's attribute according to result.

  @param Result  Point to result structure
  @param FdcDev  FDC control structure

  @retval EFI_DEVICE_ERROR - GC_TODO: Add description for return value
  @retval EFI_DEVICE_ERROR - GC_TODO: Add description for return value
  @retval EFI_DEVICE_ERROR - GC_TODO: Add description for return value
  @retval EFI_SUCCESS - GC_TODO: Add description for return value

**/
EFI_STATUS
CheckResult (
  IN     FDD_RESULT_PACKET  *Result,
  IN OUT FDC_BLK_IO_DEV     *FdcDev
  )
{
  //
  // Check Status Register0
  //
  if ((Result->Status0 & STS0_IC) != IC_NT) {
    if ((Result->Status0 & STS0_SE) == 0x20) {
      //
      // seek error
      //
      FdcDev->ControllerState->NeedRecalibrate = TRUE;
    }

    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }
  //
  // Check Status Register1
  //
  if ((Result->Status1 & (STS1_EN | STS1_DE | STS1_OR | STS1_ND | STS1_NW | STS1_MA)) != 0) {
    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }
  //
  // Check Status Register2
  //
  if ((Result->Status2 & (STS2_CM | STS2_DD | STS2_WC | STS2_BC | STS2_MD)) != 0) {
    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }

  return EFI_SUCCESS;
}

/**
  Check the drive status information.

  @param StatusRegister3  the value of Status Register 3

  @retval EFI_SUCCESS           The disk is not write protected
  @retval EFI_WRITE_PROTECTED:  The disk is write protected

**/
EFI_STATUS
CheckStatus3 (
  IN UINT8 StatusRegister3
  )
{
  if ((StatusRegister3 & STS3_WP) != 0) {
    return EFI_WRITE_PROTECTED;
  }

  return EFI_SUCCESS;
}

/**
  Calculate the number of block in the same cylinder according to LBA.

  @param FdcDev FDC_BLK_IO_DEV *: A pointer to FDC_BLK_IO_DEV
  @param LBA EFI_LBA:      The starting logic block address
  @param NumberOfBlocks UINTN: The number of blocks

  @return The number of blocks in the same cylinder which the starting
        logic block address is LBA

**/
UINTN
GetTransferBlockCount (
  IN  FDC_BLK_IO_DEV  *FdcDev,
  IN  EFI_LBA         LBA,
  IN  UINTN           NumberOfBlocks
  )
{
  UINT8 EndOfTrack;
  UINT8 Head;
  UINT8 SectorsInTrack;

  //
  // Calculate the number of block in the same cylinder
  //
  EndOfTrack      = DISK_1440K_EOT;
  Head            = (UINT8) ((UINTN) LBA / EndOfTrack % 2);

  SectorsInTrack  = (UINT8) (EndOfTrack * (2 - Head) - (UINT8) ((UINTN) LBA % EndOfTrack));
  if (SectorsInTrack < NumberOfBlocks) {
    return SectorsInTrack;
  } else {
    return NumberOfBlocks;
  }
}

/**
  When the Timer(2s) off, turn the drive's motor off.

  @param Event EFI_EVENT: Event(the timer) whose notification function is being
                     invoked
  @param Context VOID *:  Pointer to the notification function's context

**/
VOID
EFIAPI
FddTimerProc (
  IN EFI_EVENT  Event,
  IN VOID       *Context
  )
{
  FDC_BLK_IO_DEV  *FdcDev;
  UINT8           Data;

  FdcDev = (FDC_BLK_IO_DEV *) Context;

  //
  // Get the motor status
  //
  Data = FdcReadPort (FdcDev, FDC_REGISTER_DOR);

  if (((FdcDev->Disk == FdcDisk0) && ((Data & 0x10) != 0x10)) ||
      ((FdcDev->Disk == FdcDisk1) && ((Data & 0x21) != 0x21))
      ) {
    return ;
  }
  //
  // the motor is on, so need motor off
  //
  Data = 0x0C;
  Data = (UINT8) (Data | (SELECT_DRV & FdcDev->Disk));
  FdcWritePort (FdcDev, FDC_REGISTER_DOR, Data);
  MicroSecondDelay (500);
}

/**
  Read an I/O port of FDC.

  @param[in] FdcDev  A pointer to FDC_BLK_IO_DEV.
  @param[in] Offset  The address offset of the I/O port.

  @retval  8-bit data read from the I/O port.
**/
UINT8
FdcReadPort (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN UINT32          Offset
  )
{
  EFI_STATUS  Status;
  UINT8       Data;

  Status = FdcDev->IsaIo->Io.Read (
                            FdcDev->IsaIo,
                            EfiIsaIoWidthUint8,
                            FdcDev->BaseAddress + Offset,
                            1,
                            &Data
                            );
  ASSERT_EFI_ERROR (Status);

  return Data;
}

/**
  Write an I/O port of FDC.

  @param[in] FdcDev  A pointer to FDC_BLK_IO_DEV
  @param[in] Offset  The address offset of the I/O port
  @param[in] Data    8-bit Value written to the I/O port
**/
VOID
FdcWritePort (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN UINT32          Offset,
  IN UINT8           Data
  )
{
  EFI_STATUS  Status;

  Status = FdcDev->IsaIo->Io.Write (
                            FdcDev->IsaIo,
                            EfiIsaIoWidthUint8,
                            FdcDev->BaseAddress + Offset,
                            1,
                            &Data
                            );
  ASSERT_EFI_ERROR (Status);
}

